Pinvon's Blog

所见, 所闻, 所思, 所想

D3.js 基础

基本操作

添加元素

d3.select("body").append("p").text("hello world");

d3.select("body")
	.append("p")
	.text("hello world");

var body = d3.select("body");
var p = body.append("p");
p.text("hello world");

解释: 选择 body 标签, 为之添加一个 p 标签, 设置内容为 "hello world".

绑定数据

var dataset = [1, 2, 3, 4, 5];
d3.select("body").selectAll("p")  // 选择 body 中所有的 p 标签
.data(dataset)
.enter()	// 如果数值多于 DOM 元素, 就用 enter() 创建新元素的占们符
.append("p")  // 在 enter() 创建的占位符中插入 p 标签
.text("hello world");

结果会出现 dataset.length 个 "hello world".

在 .text() 内部, 也可以使用匿名函数, 对数据进行更多的操作:

d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d, i) {	... })  // d 代表数据, i 代表索引
.style("color", function(d) { 
	if (d > 3) return "red";
	else return "black";
});

添加 SVG 画布制作简单图表

var width = 300;
var height = 300;
var svg = d3.select("body")
	.append("svg")	// 添加一个 svg 元素
	.attr("width", width)
	.attr("height", height);

var dataset = [ 250, 210, 170, 130, 90];
var rectHeight = 25;  // 每个矩形所占的像素高度

svg.selectAll("rect")
	.data(dataset)
	.enter()
	.append("rect")
	.attr("x", 20)
	.attr("y", function(d, i) { return i*rectHeight; })
	.attr("width", function(d) { return d; })
	.attr("heigth", rectHeight-2)
	.attr("fill", "steelblue");

svg 中包含的基本元素有: 矩形(rect), 圆(circle), 椭圆(ellipse), 线(line), 文本(text)

比例尺

线性比例尺

var dataset = [1.2, 2.3, 0.9, 1.5, 3.3];
var min = d3.min(dataset);
var max = d3.max(dataset);
var linear = d3.scale.linear()  // 在新版本中改成 d3.scaleLinear()
	.domain([min, max])
	.range([0, 300]);

linear(0.9);  // 返回 0
linear(2.3);  // 返回 175
linear(3.3);  // 返回 300

线性比例尺就是按照 y=kx+b 的线性关系来映射的.

序数比例尺

如果比例尺不是连续的, 则可以使用序数比例尺.

var index = [0, 1, 2, 3, 4];
var color = ["red", "blue", "green", "yellow", "black"];
var ordinal = d3.scale.ordinal()
	.domain(index)
	.range(color);

ordinal(0);  // 返回 red
ordinal(2);  // 返回 green
ordinal(4);  // 返回 black

给柱形图添加比例尺

...
svg.selectAll("rect")
	.data(dataset)
	...
	.attr("y", function(d, i) { return i*rectHeight; })
	.attr("width", function(d) { return linear(d); })  // 使用比例尺
	...

这样, 所有数值都按照同一个线性比例尺的关系来计算宽度, 因此数值之间的大小关系不变.

坐标轴

要生成坐标轴, 需要用到比例尺, 两者经常一起使用.

var dataset = [2.5, 2.1, 1.7, 1.3, 0.9];
var linear = d3.scale.linear()
	.domain([0, d3.max(dataset)])
	.range([0, 250]);
var axis = d3.svg.axis()
	.scale(linear)  // 指定比例尺
	.orient("bottom")  // 指定刻度的方向, bottom 表示在坐标轴的下方显示
	.ticks(7);  // 指定刻度的数量

svg.append("g")  // 添加一个元素, g 表示 group, 相当于容器
	.attr("transform", "translate(20, 130)")  // 通过 transform 属性来设置坐标轴的位置
	.call(axis);

完整柱状图

<html>  
<head>  
	<meta charset="utf-8">  
	<title>完整的柱形图</title>  
</head> 
<style>
	.axis path,
	.axis line{
		fill: none;
		stroke: black;
		shape-rendering: crispEdges;
	}
	.axis text {
		font-family: sans-serif;
		font-size: 11px;
	}
	.MyRect {
		fill: steelblue;
	}
	.MyText {
		fill: white;
		text-anchor: middle;
	}
</style>
<body>  
	<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>  
	<script>
	//画布大小
	var width = 400;
	var height = 400;
	//在 body 里添加一个 SVG 画布	
	var svg = d3.select("body")
		.append("svg")
		.attr("width", width)
		.attr("height", height);
	//画布周边的空白
	var padding = {left:30, right:30, top:20, bottom:20};
	//定义一个数组
	var dataset = [10, 20, 30, 40, 33, 24, 12, 5];
	//x轴的比例尺
	var xScale = d3.scale.ordinal()
		.domain(d3.range(dataset.length))
		.rangeRoundBands([0, width - padding.left - padding.right]);
	//y轴的比例尺
	var yScale = d3.scale.linear()
		.domain([0,d3.max(dataset)])
		.range([height - padding.top - padding.bottom, 0]);
	//定义x轴
	var xAxis = d3.svg.axis()
		.scale(xScale)
		.orient("bottom");
	//定义y轴
	var yAxis = d3.svg.axis()
		.scale(yScale)
		.orient("left");
	//矩形之间的空白
	var rectPadding = 4;
	//添加矩形元素
	var rects = svg.selectAll(".MyRect")
		.data(dataset)
		.enter()
		.append("rect")
		.attr("class","MyRect")
		.attr("transform","translate(" + padding.left + "," + padding.top + ")")
		.attr("x", function(d,i){ return xScale(i) + rectPadding/2; })
		.attr("y",function(d){ return yScale(d); })
		.attr("width", xScale.rangeBand() - rectPadding )
		.attr("height", function(d){
			return height - padding.top - padding.bottom - yScale(d);
		});
	//添加文字元素
	var texts = svg.selectAll(".MyText")
		.data(dataset)
		.enter()
		.append("text")
		.attr("class","MyText")
		.attr("transform","translate(" + padding.left + "," + padding.top + ")")
		.attr("x", function(d,i){ return xScale(i) + rectPadding/2; })
		.attr("y",function(d){ return yScale(d); })
		.attr("dx",function(){ return (xScale.rangeBand() - rectPadding)/2;	})
		.attr("dy",function(d){	return 20; })
		.text(function(d){ return d; });
	//添加x轴
	svg.append("g")
		.attr("class","axis")
		.attr("transform","translate(" + padding.left + "," + (height - padding.bottom) + ")")
		.call(xAxis); 
	//添加y轴
	svg.append("g")
		.attr("class","axis")
		.attr("transform","translate(" + padding.left + "," + padding.top + ")")
		.call(yAxis);
</script>  
</body>  
</html>

动画

D3 提供了 4 个方法用于实现图形的过渡.

transition()

启动过渡效果.

.attr("fill", "red")  // 初始颜色为红色
.transition()
.attr("fill", "steelblue")  // 终止颜色为蓝色

duration()

指定过渡的持续时间, 单位为毫秒. 如 duration(2000).

ease()

指定过渡的方式. 常用方式有: linear(线性变化), circle(慢慢到达变换的最终状态), elastic(带有弹跳的到达最终状态), blounce(在最终状态处弹跳几下)

调用: ease("bounce");

delay()

指定延迟时间, 表示一定时间后才开始转变, 单位为毫秒.

.transition()
.duration(1000)
.delay(500)

再如:

.transition()
.duration(1000)
.delay(function(d, i){ return 200*i; })

例子

(1) 移动 x 坐标.

var circle1 = svg.append("circle")
        .attr("cx", 100)
        .attr("cy", 100)
        .attr("r", 45)
        .style("fill","green");

//在1秒(1000毫秒)内将圆心坐标由100变为300
circle1.transition()
    .duration(1000)
    .attr("cx", 300);

(2) 移动 x 坐标, 改变颜色.

var circle2 = svg.append("circle")... //与第一个圆一样,省略部分代码

//在1.5秒(1500毫秒)内将圆心坐标由100变为300,
//将颜色从绿色变为红色
circle2.transition()
    .duration(1500)
    .attr("cx", 300)
    .style("fill","red");

(3) 移动 x 坐标, 改变颜色, 改变半径.

var circle3 = svg.append("circle")... //与第一个圆一样,省略部分代码

//在2秒(2000毫秒)内将圆心坐标由100变为300
//将颜色从绿色变为红色
//将半径从45变成25
//过渡方式采用bounce(在终点处弹跳几次)
circle3.transition()
    .duration(2000)
    .ease("bounce")
    .attr("cx", 300)
    .style("fill","red")
    .attr("r", 25);

update, enter 和 exit

update, enter 和 exit 主要用于处理当选择集和数据的数量关系不确定的情况.

如, 有数据, 但没有足够图形元素的时候, 使用此方法添加足够的元素:

svg.selectAll("rect")   //选择svg内所有的矩形
    .data(dataset)      //绑定数组
    .enter()            //指定选择集的enter部分
    .append("rect")     //添加足够数量的矩形元素

如果数组为 [3,6,9,12,15], 将此数组绑定到三个 p 元素的选择集上, 则会有两个数据没有元素与之对应, 这一部分称为 enter, 有元素与数据对应的部分称为 update, 如果数组元素数量少于选择集数量, 则没有数据绑定的部分称为 exit. 如下图所示:

0.png

update 和 enter 的使用

绑定数据数量 > 对应元素时, 需要使用 append() 添加元素. 如:

var dataset = [ 3 , 6 , 9 , 12 , 15 ];

//选择body中的p元素
var p = d3.select("body").selectAll("p");

//获取update部分
var update = p.data(dataset);

//获取enter部分
var enter = update.enter();

//update部分的处理:更新属性值
update.text(function(d){
    return "update " + d;
});

//enter部分的处理:添加元素后赋予属性值
enter.append("p")
    .text(function(d){
        return "enter " + d;
    });

update 和 exit 的使用

绑定数据量 < 对应元素时, 需要删除多余的元素. 如:

var dataset = [ 3 ];

//选择body中的p元素
var p = d3.select("body").selectAll("p");

//获取update部分
var update = p.data(dataset);

//获取exit部分
var exit = update.exit();

//update部分的处理:更新属性值
update.text(function(d){
    return "update " + d;
});

//exit部分的处理:修改p元素的属性
exit.text(function(d){
        return "exit";
    });

//exit部分的处理通常是删除元素
// exit.remove();

交互式操作

在图形元素上设置一个或多个监听器, 当事件发生时, 做出相应的反应.

添加交互的例子:

var circle = svg.append("circle");

circle.on("click", function(){
    //在这里添加交互内容
});

在 D3 中, 每一个选择集都有 on() 方法, 用于添加事件监听器. 第一个参数是监听的事件, 第二个参数是监听到事件后响应的内容, 是一个函数.

鼠标常用事件:

事件 描述
click 单击, 相当于 mousedown 和 mouseup
mouseover 光标放在某元素上
mouseout 光标从某元素上移出来时
mousemove 鼠标被移动的时候
mousedown 鼠标按钮被按下
mouseup 鼠标按钮被松开
dbclick 鼠标双击

键盘常用的事件为:

keydown 当用户按下任意键时触发, 按住不放会重复触发
keypress 当用户按下字符键时触发, 按住不放会重复触发
keyup 当用户释放时触发

触屏常用的事件为:

touchstart 当触摸点被放在触摸屏上时触发
touchmove 当触摸点在触摸屏上移动时触发
touchend 当触摸点从触摸屏上拿开时触发

例子

为前面的柱状图加上交互事件.

var rects = svg.selectAll(".MyRect")
        .data(dataset)
        .enter()
        .append("rect")
        .attr("class","MyRect")   //把类里的 fill 属性清空
        .attr("transform","translate(" + padding.left + "," + padding.top + ")")
        .attr("x", function(d,i){
            return xScale(i) + rectPadding/2;
        } )
        .attr("y",function(d){
            return yScale(d);
        })
        .attr("width", xScale.rangeBand() - rectPadding )
        .attr("height", function(d){
            return height - padding.top - padding.bottom - yScale(d);
        })
        .attr("fill","steelblue")       //填充颜色不要写在CSS里
        .on("mouseover",function(d,i){
            d3.select(this)
                .attr("fill","yellow");
        })
        .on("mouseout",function(d,i){
            d3.select(this)
                .transition()
                .duration(500)
                .attr("fill","steelblue");
        });

布局

力导向图

力导向图是绘图的一种算法, 在二维或三维空间里配置节点, 节点之间用线连接, 称为连线. 各连线的长度几乎相等, 且尽可能不相交. 节点和连线都被施加了力的作用, 力是根据节点和连线的相对位置计算的. 根据力的作用, 来计算节点和连线的运动轨迹, 并不断降低它们的能量, 最终达到一种能量很低的安定状态.

var nodes = [ { name: "桂林" }, { name: "广州" },
              { name: "厦门" }, { name: "杭州" },
              { name: "上海" }, { name: "青岛" },
              { name: "天津" } ];

 var edges = [ { source : 0 , target: 1 } , { source : 0 , target: 2 } ,
               { source : 0 , target: 3 } , { source : 1 , target: 4 } ,
               { source : 1 , target: 5 } , { source : 1 , target: 6 } ];

// 定义一个力导向图的布局
var force = d3.layout.force()
      .nodes(nodes) //指定节点数组
      .links(edges) //指定连线数组
      .size([width,height]) //指定作用域范围
      .linkDistance(150) //指定连线长度
      .charge([-400]); //相互之间的作用力

// 使力学作用生效
force.start();

// 绘制
//添加连线 
 var svg_edges = svg.selectAll("line")
     .data(edges)
     .enter()
     .append("line")
     .style("stroke","#ccc")
     .style("stroke-width",1);

 var color = d3.scale.category20();

 //添加节点 
 var svg_nodes = svg.selectAll("circle")
     .data(nodes)
     .enter()
     .append("circle")
     .attr("r",20)
     .style("fill",function(d,i){
         return color(i);
     })
     .call(force.drag);  //使得节点能够拖动

 //添加描述节点的文字
 var svg_texts = svg.selectAll("text")
     .data(nodes)
     .enter()
     .append("text")
     .style("fill", "black")
     .attr("dx", 20)
     .attr("dy", 8)
     .text(function(d){
        return d.name;
     });

// 不断更新节点和连线的位置
force.on("tick", function(){ //对于每一个时间间隔
    //更新连线坐标
    svg_edges.attr("x1",function(d){ return d.source.x; })
        .attr("y1",function(d){ return d.source.y; })
        .attr("x2",function(d){ return d.target.x; })
        .attr("y2",function(d){ return d.target.y; });

    //更新节点坐标
    svg_nodes.attr("cx",function(d){ return d.x; })
        .attr("cy",function(d){ return d.y; });

    //更新文字坐标
    svg_texts.attr("x", function(d){ return d.x; })
       .attr("y", function(d){ return d.y; });
 });

读取 csv 文件

由于 Web 浏览器不支持直接加载本地数据, 因此, 我们可以搭建本地 Web 服务器.

在终端进入到 html 文件的根目录, 输入:

python -m SimpleHTTPServer 8888 &

然后在浏览器中输入: http://localhost:8888/index.html

读取 csv 文件的代码如下:

d3.csv("http://localhost:8888/data/test.csv",function(error,data){
    if(error){
        console.log(error);
    } else {  //(3)取出其中的数字,和类别名
        for(var i=0;i<data.length;i++){
            numset.push(parseFloat(data[i].Column1_name));
            nameset.push(data[i].Column2_name);
        }
        var pie=d3.layout.pie(numset);// (4)numset转化数据为适合生成饼图的对象数组
        // 略,使用加载的数据画圆环图
    }
});

Comments

使用 Disqus 评论
comments powered by Disqus